nestjs教程
资源
- 文档地址
让我们用Nestjs来重写一个CNode https://github.com/jiayisheji/blog/issues/19
Nest.js 从零到壹系列
知识点列表
安装
$ npm i -g @nestjs/cli
$ nest new project-name
使用cli创建模块
创建一个模块
nest g resource xxx
创建一个service
nest g service xxx
创建一个
全局模块
- 创建全局模块
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
- 在需要的地方导入service就可以使用了
日志系统
安装模块
npm i log4js stacktrace-js -S
创建配置文件
// util/log4js.config.ts import * as path from 'path'; const baseLogPath = path.resolve(__dirname, '../../logs'); // 日志要写入哪个目录 const log4jsConfig = { appenders: { console: { type: 'console', // 会打印到控制台 }, access: { type: 'dateFile', // 会写入文件,并按照日期分类 filename: `${baseLogPath}/access/access.log`, // 日志文件名,会命名为:access.20200320.log alwaysIncludePattern: true, pattern: 'yyyyMMdd', daysToKeep: 60, numBackups: 3, category: 'http', keepFileExt: true, // 是否保留文件后缀 }, app: { type: 'dateFile', filename: `${baseLogPath}/app-out/app.log`, alwaysIncludePattern: true, layout: { type: 'pattern', pattern: '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}', }, // 日志文件按日期(天)切割 pattern: 'yyyyMMdd', daysToKeep: 60, // maxLogSize: 10485760, numBackups: 3, keepFileExt: true, }, errorFile: { type: 'dateFile', filename: `${baseLogPath}/errors/error.log`, alwaysIncludePattern: true, layout: { type: 'pattern', pattern: '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}', }, // 日志文件按日期(天)切割 pattern: 'yyyyMMdd', daysToKeep: 60, // maxLogSize: 10485760, numBackups: 3, keepFileExt: true, }, errors: { type: 'logLevelFilter', level: 'ERROR', appender: 'errorFile', }, }, categories: { default: { appenders: ['console', 'app', 'errors'], level: 'DEBUG', }, info: { appenders: ['console', 'app', 'errors'], level: 'info' }, access: { appenders: ['console', 'app', 'errors'], level: 'info' }, http: { appenders: ['access'], level: 'DEBUG' }, }, pm2: true, // 使用 pm2 来管理项目时,打开 pm2InstanceVar: 'INSTANCE_ID', // 会根据 pm2 分配的 id 进行区分,以免各进程在写日志时造成冲突 }; export default log4jsConfig;
创建实例
import * as Path from 'path'; import * as Log4js from 'log4js'; import * as Util from 'util'; import * as Moment from 'moment'; // 处理时间的工具 import * as StackTrace from 'stacktrace-js'; import Chalk from 'chalk'; import config from './log4js.config'; // 日志级别 export enum LoggerLevel { ALL = 'ALL', MARK = 'MARK', TRACE = 'TRACE', DEBUG = 'DEBUG', INFO = 'INFO', WARN = 'WARN', ERROR = 'ERROR', FATAL = 'FATAL', OFF = 'OFF', } // 内容跟踪类 export class ContextTrace { constructor( public readonly context: string, public readonly path?: string, public readonly lineNumber?: number, public readonly columnNumber?: number, ) {} } Log4js.addLayout('Awesome-nest', (logConfig: any) => { return (logEvent: Log4js.LoggingEvent): string => { let moduleName: string = ''; let position: string = ''; // 日志组装 const messageList: string[] = []; logEvent.data.forEach((value: any) => { if (value instanceof ContextTrace) { moduleName = value.context; // 显示触发日志的坐标(行,列) if (value.lineNumber && value.columnNumber) { position = `${value.lineNumber}, ${value.columnNumber}`; } return; } if (typeof value !== 'string') { value = Util.inspect(value, false, 3, true); } messageList.push(value); }); // 日志组成部分 const messageOutput: string = messageList.join(' '); const positionOutput: string = position ? ` [${position}]` : ''; const typeOutput: string = `[${logConfig.type}] ${logEvent.pid.toString()} - `; const dateOutput: string = `${Moment(logEvent.startTime).format('YYYY-MM-DD HH:mm:ss')}`; const moduleOutput: string = moduleName ? `[${moduleName}] ` : '[LoggerService] '; let levelOutput: string = `[${logEvent.level}] ${messageOutput}`; // 根据日志级别,用不同颜色区分 switch (logEvent.level.toString()) { case LoggerLevel.DEBUG: levelOutput = Chalk.green(levelOutput); break; case LoggerLevel.INFO: levelOutput = Chalk.cyan(levelOutput); break; case LoggerLevel.WARN: levelOutput = Chalk.yellow(levelOutput); break; case LoggerLevel.ERROR: levelOutput = Chalk.red(levelOutput); break; case LoggerLevel.FATAL: levelOutput = Chalk.hex('#DD4C35')(levelOutput); break; default: levelOutput = Chalk.grey(levelOutput); break; } return `${Chalk.green(typeOutput)}${dateOutput} ${Chalk.yellow(moduleOutput)}${levelOutput}${positionOutput}`; }; }); // 注入配置 Log4js.configure(config); // 实例化 const logger = Log4js.getLogger(); logger.level = LoggerLevel.TRACE; export class Logger { static trace(...args) { logger.trace(Logger.getStackTrace(), ...args); } static debug(...args) { logger.debug(Logger.getStackTrace(), ...args); } static log(...args) { logger.info(Logger.getStackTrace(), ...args); } static info(...args) { logger.info(Logger.getStackTrace(), ...args); } static warn(...args) { logger.warn(Logger.getStackTrace(), ...args); } static warning(...args) { logger.warn(Logger.getStackTrace(), ...args); } static error(...args) { logger.error(Logger.getStackTrace(), ...args); } static fatal(...args) { logger.fatal(Logger.getStackTrace(), ...args); } static access(...args) { const loggerCustom = Log4js.getLogger('http'); loggerCustom.info(Logger.getStackTrace(), ...args); } // 日志追踪,可以追溯到哪个文件、第几行第几列 static getStackTrace(deep: number = 2): string { const stackList: StackTrace.StackFrame[] = StackTrace.getSync(); const stackInfo: StackTrace.StackFrame = stackList[deep]; const lineNumber: number = stackInfo.lineNumber; const columnNumber: number = stackInfo.columnNumber; const fileName: string = stackInfo.fileName; const basename: string = Path.basename(fileName); return `${basename}(line: ${lineNumber}, column: ${columnNumber}): \n`; } }
安装中间件
nest g middleware logger middleware
配置中间件
// logger.middleware.ts import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response } from 'express'; import { Logger } from '../common/util/log4js'; // 函数式中间件 export function logger(req: Request, res: Response, next: () => any) { const code = res.statusCode; // 响应状态码 next(); // 组装日志信息 const logFormat = ` >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Request original url: ${req.originalUrl} Method: ${req.method} IP: ${req.ip} Status code: ${code} Parmas: ${JSON.stringify(req.params)} Query: ${JSON.stringify(req.query)} Body: ${JSON.stringify(req.body)} \n >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> `; // 根据状态码,进行日志类型区分 if (code >= 500) { Logger.error(logFormat); } else if (code >= 400) { Logger.warn(logFormat); } else { Logger.access(logFormat); Logger.log(logFormat); } }
应用中间件
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { logger } from './middleware/logger.middleware'; import * as express from 'express'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(express.urlencoded({ extended: true })); app.use(logger); app.useGlobalPipes(new ValidationPipe()); // 配置 Swagger const options = new DocumentBuilder() .setTitle('film-server') .setDescription('电影服务器接口') .setVersion('1.0') // .addTag('test') // 添加分组标签 .build(); const document = SwaggerModule.createDocument(app, options); SwaggerModule.setup('docs', app, document); await app.listen(9999); console.log("服务已启动, 在http://localhost:9999") } bootstrap();
Swagger文档
安装
npm i @nestjs/swagger swagger-ui-express -S
main.ts配置
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); // 配置 Swagger const options = new DocumentBuilder() .setTitle('film-server') .setDescription('电影服务器接口') .setVersion('1.0') // .addTag('test') // 添加分组标签 .build(); const document = SwaggerModule.createDocument(app, options); SwaggerModule.setup('docs', app, document); await app.listen(9999); console.log("服务已启动, 在http://localhost:9999") } bootstrap();
添加参数
import { IsString, IsNumberString, IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class CreateUserDto { @IsNotEmpty({ message: 'username不能为空' }) @IsString({ message: 'username必须是字符类型' }) @ApiProperty() readonly username: string; @IsNumberString({ message: 'age必须是数字或者字符串' }) @ApiProperty() age: number | string; }
分组标签
import { Controller, Post, Body, UseGuards, UsePipes } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from '../auth/auth.service'; import { UserService } from './user.service'; import { ValidationPipe } from '../../pipe/validation.pipe'; import { RegisterInfoDTO } from './user.dto'; import { ApiTags } from '@nestjs/swagger'; @ApiTags('user') // 添加 接口标签 装饰器 @Controller('user') export class UserController { constructor(private readonly authService: AuthService, private readonly usersService: UserService) {} // JWT验证 - Step 1: 用户请求登录 @Post('login') async login(@Body() loginParmas: any) { ... } @UseGuards(AuthGuard('jwt')) @UsePipes(new ValidationPipe()) @Post('register') async register(@Body() body: RegisterInfoDTO) { return await this.usersService.register(body); }
添加参数校验
https://blog.csdn.net/kuangshp128/article/details/97132480
一、局部验证的方式
安装包
npm i --save class-validator class-transformer
比如在
cat
的目录下创建一个dto
的文件夹,创建一个create.cat.dto.ts
的文件import { IsString, IsInt, MinLength, MaxLength } from 'class-validator'; export class CreateCatDto { @IsString({ message: '必须的字符类型' }) @MinLength(2, { message: '长度不能小于2', }) @MaxLength(10, { message: '长度不能超过10', }) readonly name: string; @IsInt({ message: '必须的整数' }) readonly age: number; }
在
cat.controller.ts
中使用提交过来的数据约束@Controller('cat') export class CatController { constructor(private readonly catService: CatService) {} @Post() @HttpCode(HttpStatus.CREATED) async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) { Logger.log('-----------创建猫 start--------------'); Logger.log(createCatDto); Logger.log('-----------创建猫 end--------------'); return '创建猫'; } ... }
二、全局使用管道校验(前提是先安装包)
在根目录下的
main.ts
文件中import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { Logger, ValidationPipe } from '@nestjs/common'; import * as helmet from 'helmet'; async function bootstrap() { const app = await NestFactory.create(AppModule, { cors: true, // 设置跨站访问 logger: false, }); // 使用跨站脚本攻击类的库 app.use(helmet()); // 给请求添加prefix app.setGlobalPrefix('api/v1'); // 全局使用管道 app.useGlobalPipes(new ValidationPipe()); await app.listen(3000, () => { Logger.log('服务已经启动,请访问localhost:3000'); }); } bootstrap();
在
cat.controller.ts
中使用提交过来的数据约束@Controller('cat') export class CatController { constructor(private readonly catService: CatService) {} @Post() @HttpCode(HttpStatus.CREATED) async create(@Body() createCatDto: CreateCatDto) { // 这里就不需要使用校验约束 Logger.log('-----------创建猫 start--------------'); Logger.log(createCatDto); Logger.log('-----------创建猫 end--------------'); return '创建猫'; } ... }